<script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue';

const props = defineProps({
  /**
   * @属性 传入数据
   * @类型 {
   *  background: { src: '/bg.png', x: 0, y: 0, width: 1311, height: 981 }
   *  stages: [{ sn: 1, src: '/a.png', x: 30, y: 60, width: 80, height: 80 }, ...]
   * }
   */
  data: { type: Object, required: true },
  /** @属性 当前缩放（设置初始缩放值，需要 >= 1，为大图做准备） */
  scaleNum: { type: Number, default: 1 },
  /** @属性 最大缩放 */
  scaleMax: { type: Number, default: 2 },
  /** @属性 最小缩放 */
  scaleMin: { type: Number, default: 0.6 },
  /** @属性 缩放步数 */
  scaleStep: { type: Number, default: 0.1 },
});

const emits = defineEmits(['stage', 'hide']);

const box = ref(null); // 引用# 容器

// 画布
let canvas = null;
let canvasCtx = null;
// 双缓冲画布
let buffer = null;
let bufferCtx = null;
// 大小
let size = { width: 0, height: 0 };
// 编号大小
let snSize = {
  radius: 15,
  lineWidth: 2,
  lineColor: '#e7b700',
  fontSize: 16,
  fontColor: '#e7b700',
  backgroundColor: 'white',
};
// 背景大小
let backgroundSize = { width: 0, height: 0 };
// 图层列表
let layers = [];
// 遮罩图层
let layerMask = null;
// 选中图层
let layerActive = null;
// 是否按下
let isDown = false;
// 是否拖动
let isDrag = false;
// 是否缩放
let isZoom = false;
// 按下坐标
let downPoint = { x: 0, y: 0 };
// 移动位置
let movePostion = { x: 0, y: 0 };
// 偏移位置
let offsetPostion = { x: 0, y: 0 };
// 按下手指距离
let downDistance = 0;
// 缩放比例
let scale = 1;
// 数据变更刷新
let runRefresh = Throttle(async () => {
  emits('hide');
  layerActive = null;
  layers = await GetLayers();
}, 300);

watch(props.data, () => runRefresh(), { immediate: true, deep: true });

onMounted(() => {
  // 获取实际宽高设置坐标
  onResize();
  // 判断是否需要监听窗口变化
  window.addEventListener('resize', onResize);
  // 初始化画布
  onInit();
})

onUnmounted(() => {
  // 判断是否需要监听窗口变化
  window.removeEventListener('resize', onResize);
  // 移除鼠标事件
  canvas.removeEventListener('mousedown', onMousedown);
  canvas.removeEventListener('mousemove', onMousemove);
  canvas.removeEventListener('mouseup', onMouseup);
  canvas.removeEventListener('touchstart', onMousedown);
  canvas.removeEventListener('touchmove', onMousemove);
  canvas.removeEventListener('touchend', onMouseup);
  canvas.removeEventListener('wheel', onWheel);
  document.removeEventListener('mouseup', onMouseup);
  document.removeEventListener('touchend', onMouseup);
})

/** @事件 大小变更情况发生 */
function onResize() {
  const { width, height } = box.value.getBoundingClientRect();
  size = { width, height };
  if (canvas) {
    canvas.width = width;
    canvas.height = height;
  }
}

/** @事件 初始化 */
function onInit() {
  console.log('初始化');
  canvas = document.createElement('canvas');
  buffer = document.createElement('canvas');
  buffer.width = size.width;
  buffer.height = size.height;
  canvas.width = size.width;
  canvas.height = size.height;
  canvasCtx = canvas.getContext('2d');
  bufferCtx = buffer.getContext('2d');
  canvas.style.width = '100%';
  canvas.style.height = '100%';
  box.value.firstChild && box.value.removeChild(box.value.firstChild);
  box.value.appendChild(canvas);

  // 设置初始缩放
  scale = props.scaleNum;

  // 监听鼠标事件
  canvas.addEventListener('mousedown', onMousedown);
  canvas.addEventListener('mousemove', onMousemove);
  canvas.addEventListener('mouseup', onMouseup);
  canvas.addEventListener('touchstart', onMousedown);
  canvas.addEventListener('touchmove', onMousemove);
  canvas.addEventListener('touchend', onMouseup);
  canvas.addEventListener('wheel', onWheel);
  document.addEventListener('mouseup', onMouseup);
  document.addEventListener('touchend', onMouseup);

  // 开始定时绘制, 使用requestAnimationFrame
  requestAnimationFrame(onDraw);
}

/** @事件 开始绘制 */
function onDraw() {
  // 缩放画布
  buffer.width = backgroundSize.width;
  buffer.height = backgroundSize.height;

  // 判断大小
  if (buffer.width > 0 || buffer.height > 0) {
    // 绘制到缓冲画布
    (layerActive ? [
      ...layers, // 绘制（背景+地块）+遮罩+选中地块
      layerMask,
      layerActive,
    ] : layers).forEach((layer) => {
      layer && bufferCtx.drawImage(
        layer.canvas,
        layer.x,
        layer.y,
        layer.width,
        layer.height,
      );
    });

    // 绘制缓存画布到画布（等比例缩放）
    canvasCtx.drawImage(
      buffer,
      offsetPostion.x / scale, // 源图像的 x 坐标
      offsetPostion.y / scale, // 源图像的 y 坐标
      size.width / scale, // 源图像的宽度
      size.height / scale, // 源图像的高度
      0, // 目标图像的 x 坐标
      0, // 目标图像的 y 坐标
      size.width, // 目标图像的宽度
      size.height // 目标图像的高度
    );
  }

  // 递归调用
  requestAnimationFrame(onDraw);
}

/** @事件 放大缩小 */
function onZoom(zoom) {
  if (layerActive) { return; }
  if (props.scaleStep > 0) {
    // 放大缩小比例
    if (zoom) {
      scale += props.scaleStep;
    } else {
      scale -= props.scaleStep;
    }
    // 限制最小缩放比例
    if (scale < props.scaleMin) {
      scale = props.scaleMin;
    }
    // 限制最大缩放比例
    if (scale > props.scaleMax) {
      scale = props.scaleMax;
    }
    // 更新偏移量
    const newOffset = GetNewOffset(offsetPostion);
    offsetPostion.x = newOffset.x;
    offsetPostion.y = newOffset.y;
  }
}

/** @事件 鼠标滚动 */
function onWheel(e) {
  if (e.deltaY < 0) {
    onZoom(true);
  } else {
    onZoom(false);
  }
}

/** @事件 鼠标按下 */
function onMousedown(e) {
  const touches = GetTouchs(e);
  if (touches.length === 1) {
    // console.log('鼠标按下');
    downPoint = GetEventXY(e);
    isDrag = false;
    isDown = true;
  } else if (touches.length === 2) {
    // console.log('双指按下');
    downDistance = GetDistance(touches);
    isDrag = false;
    isZoom = true;
  }
}

/** @事件 鼠标移动 */
function onMousemove(e) {
  if (isZoom) {
    // 双指缩放
    const newTouches = GetTouchs(e);
    // 计算历史手指距离
    const newDistance = GetDistance(newTouches);
    // 判断放大还是缩小
    if (newDistance > downDistance) {
      onZoom(true);
    } else {
      onZoom(false);
    }
  } else if (isDown) {
    // 根据坐标计算拖动坐标，向上向左为负，向下向右为正
    const { x, y } = GetEventXY(e);
    // 计算偏移量
    const newOffset = GetNewOffset({
      x: movePostion.x + (downPoint.x - x),
      y: movePostion.y + (downPoint.y - y),
    });
    // 判断是否拖动
    if (newOffset.x !== movePostion.x || newOffset.y !== movePostion.y) {
      if (!layerActive) { // 选中地块的时候不允许拖动
        offsetPostion.x = newOffset.x;
        offsetPostion.y = newOffset.y;
      }
      isDrag = true;
    }
  }
}

/** @事件 鼠标抬起 */
function onMouseup(e) {
  // 如果是画布按下后任意位置抬起则阻断冒泡和默认事件
  if (isDown) {
    e.stopPropagation();
    e.preventDefault();

    // 记录最后一次挪动
    movePostion.x = offsetPostion.x;
    movePostion.y = offsetPostion.y;

    // 判断是否点击到地块
    if (isDrag === false && isZoom === false) {
      if (layerActive) {
        layerActive = null;
        emits('hide');
      } else {
        // 通过layers[?].canvas的透明度来判断
        for (let i = (layers.length - 1); i > 0; i --) {
          const layer = layers[i];
          // 获取点击位置的像素信息，判断透明度是否为透明地块
          const pixel = layer.canvas.getContext('2d').getImageData(
            ((downPoint.x + offsetPostion.x) / scale) - layer.x,
            ((downPoint.y + offsetPostion.y) / scale) - layer.y,
            1,
            1
          ).data;
          // 判断透明度是否为0 Uint8ClampedArray(4) [0, 0, 0, 0]
          if (
            pixel[0] > 0 ||
            pixel[1] > 0 ||
            pixel[2] > 0 ||
            pixel[3] > 0
          ) {
            // 设置按下图层
            layerActive = layer;
            // 判断点击土地的位置，是否位于画板可视区域的左边或上边
            const isLeft = downPoint.x < canvas.width / 2;
            const isTop = downPoint.y < canvas.height / 2;
            // 触发事件
            emits('stage', {
              ...layer,
              isLeft,
              isTop
            });
            break;
          }
        }
      }
    }

    isDown = false;
    isDrag = false;
    isZoom = false;
  }
}

/** @辅助 获取遮罩 */
function GetLayerMask() {
  return CreateLayer({
    width: backgroundSize.width,
    height: backgroundSize.height,
    onDraw: (ctx, info) => {
      ctx.fillStyle = props.data?.mask?.backgroundColor || 'rgba(0, 0, 0, 0.5)';
      ctx.fillRect(0, 0, info.width, info.height);
    },
  });
}

/** @辅助 创建图层 */
async function GetLayers() {
  const { background = {}, stages = [], sn = {} } = props.data || {};
  snSize = {
    radius: sn.radius || 15,
    lineWidth: sn.lineWidth || 2,
    lineColor: sn.lineColor || '#e7b700',
    fontSize: sn.fontSize || 16,
    fontColor: sn.fontColor || '#e7b700',
    backgroundColor: sn.backgroundColor || 'white',
  };
  backgroundSize = {
    width: background.width || size.width,
    height: background.height || size.height,
  };

  // 创建图层遮罩
  layerMask = await GetLayerMask();

  const newLayers = [];
  const newStages = [background, ...stages];
  for (let i = 0; i < newStages.length; i ++) {
    const stage = newStages[i];
    const newLayer = await CreateLayer(stage);
    newLayers.push(newLayer);
  }
  return newLayers;
}

/** @辅助 创建图层 */
function CreateLayer(option) {
  return new Promise((resolve) => {
    const node = { ...option };
    // 默认参数
    if (node.x === undefined) {node.x = 0;}
    if (node.y === undefined) {node.y = 0;}
    if (node.width === undefined) {node.width = 0;}
    if (node.height === undefined) {node.height = 0;}
    // 创建画板
    node.canvas = document.createElement('canvas');
    node.canvas.width = node.width;
    node.canvas.height = node.height;
    const ctx = node.canvas.getContext('2d');
    const drawHandler = () => {
      if (node.onDraw) {
        node.onDraw(ctx, node);
      }
      if (node.sn) {
        // 绘制空心圆,空心是白色, 且中间绘制文本
        const centerX = (node.width / 2);
        const centerY = (node.height / 2);
        ctx.beginPath();
        ctx.arc(centerX, centerY, snSize.radius, 0, 2 * Math.PI); // 需要计算半径
        ctx.fillStyle = snSize.lineColor;
        ctx.fill();
        ctx.beginPath();
        ctx.arc(centerX, centerY, snSize.radius - snSize.lineWidth, 0, 2 * Math.PI); // 需要计算半径
        ctx.fillStyle = snSize.backgroundColor;
        ctx.fill();
        ctx.beginPath();
        ctx.fillStyle = snSize.fontColor;
        ctx.font = `bold ${snSize.fontSize}px Arial`; // 需要计算字体大小
        const textX = centerX - (ctx.measureText(node.sn).width / 2); // 需要计算
        const textY = centerY + (snSize.fontSize / 2.8); // 需要计算
        ctx.fillText(node.sn, textX, textY);
      }
      resolve(node);
    };
    if (node.src) {
      // 加载图片
      const img = new Image();
      img.src = node.src;
      img.addEventListener('load', () => {
        ctx.drawImage(img, 0, 0, node.width, node.height);
        drawHandler();
      });
      img.addEventListener('error', () => {
        drawHandler();
      });
    } else {
      drawHandler();
    }
    return node;
  });
}

/** @辅助 获取新偏移量 */
function GetNewOffset({ x, y }) {
  // 根据画布大小和背景大小计算最大值
  const scalcMax = GetScalcMax();
  // 限制最小值最大值
  return {
    x: x <= 0 ? 0 : x >= scalcMax.x ? scalcMax.x : x,
    y: y <= 0 ? 0 : y >= scalcMax.y ? scalcMax.y : y,
  }
}

/** @辅助 获取缩放后最大值 */
function GetScalcMax() {
  const maxX = backgroundSize.width * scale;
  const maxY = backgroundSize.height * scale;
  return {
    x: size.width > maxX ? 0 : maxX - size.width,
    y: size.height > maxY ? 0 : maxY - size.height,
  };
}

/** @辅助 获取移动端缩放距离 */
function GetDistance(touches) {
  return Math.sqrt(
    Math.pow(touches[0].x - touches[1].x, 2) +
      Math.pow(touches[0].y - touches[1].y, 2)
  );
}

/** @辅助 获取移动端放大手指 */
function GetTouchs(e) {
  // 获取基于画板的相对位置
  const { left, top } = canvas.getBoundingClientRect();
  // 获取两个手指的坐标
  return Array.from(e.touches ? e.touches : e.changedTouches ? e.changedTouches : [e]).map((touch) => {
    const { clientX, clientY } = touch;
    return {
      x: clientX - left,
      y: clientY - top,
    };
  });
}

/** @辅助 获取XY坐标 */
function GetEventXY(e) {
  // 获取鼠标位置
  const { clientX, clientY } = e.touches ? e.touches[0] : e.changedTouches ? e.changedTouches[0] : e;
  // 获取基于画板的相对位置
  const { left, top } = canvas.getBoundingClientRect();
  // 返回缩放后的相对位置
  return {
    x: clientX - left,
    y: clientY - top,
  };
}

/** @辅助 节流函数 */
function Throttle(fn, delay = 10) {  
  let timerId;  
  return function (...args) {  
    if (timerId) {  
      clearTimeout(timerId);  
    }  
    timerId = setTimeout(() => {  
      fn.apply(this, args);  
      timerId = null;  
    }, delay);  
  } 
}

</script>

<template>
  <div class="MapSelect" ref="box"></div>
</template>

<!-- 允许自定义宽高通过CSS进行控制 -->
<style scoped>
.MapSelect {
  width: 100%;
  height: 100%;
  background-color: black;
}
</style>